use crate::utils::type_like::{TypeAnnotation, TypeItem};
use crate::utils::{AnnotationCodegen, CloningPolicy, CloningPolicyFlags};
use convert_case::{Case, Casing as _};
use darling::util::Flag;
use quote::{format_ident, quote, quote_spanned};
use syn::parse_quote;

#[derive(darling::FromMeta, Debug, Clone)]
/// The available options for `#[pavex::config]`.
pub struct InputSchema {
    pub id: Option<syn::Ident>,
    pub pavex: Option<syn::Ident>,
    pub key: String,
    pub clone_if_necessary: Flag,
    pub never_clone: Flag,
    pub default_if_missing: Flag,
    pub include_if_unused: Flag,
}

impl TryFrom<InputSchema> for Properties {
    type Error = darling::Error;

    fn try_from(input: InputSchema) -> Result<Self, Self::Error> {
        let InputSchema {
            id,
            pavex,
            key,
            clone_if_necessary,
            never_clone,
            default_if_missing,
            include_if_unused,
        } = input;
        let Ok(cloning_policy) = CloningPolicyFlags {
            clone_if_necessary,
            never_clone,
        }
        .try_into() else {
            return Err(darling::Error::custom(
                "A configuration type can't have multiple cloning strategies. You can only specify *one* of `never_clone` and `clone_if_necessary`.",
            ));
        };

        Ok(Properties {
            id,
            pavex,
            key,
            cloning_policy,
            default_if_missing: default_if_missing.is_present(),
            include_if_unused: include_if_unused.is_present(),
        })
    }
}

#[derive(darling::FromMeta, Debug, Clone, PartialEq, Eq)]
pub struct Properties {
    pub id: Option<syn::Ident>,
    pub pavex: Option<syn::Ident>,
    pub key: String,
    pub cloning_policy: Option<CloningPolicy>,
    pub default_if_missing: bool,
    pub include_if_unused: bool,
}

pub struct ConfigAnnotation;

impl TypeAnnotation for ConfigAnnotation {
    const PLURAL_COMPONENT_NAME: &str = "Configuration types";

    const ATTRIBUTE: &str = "#[pavex::config]";

    type InputSchema = InputSchema;

    fn codegen(
        metadata: Self::InputSchema,
        item: TypeItem,
    ) -> Result<AnnotationCodegen, proc_macro::TokenStream> {
        let properties = Properties::try_from(metadata).map_err(|e| e.write_errors())?;
        emit(item, properties).map_err(|e| e.write_errors().into())
    }
}

/// Decorate the input with a `#[diagnostic::pavex::config]` attribute
/// that matches the provided properties.
fn emit(raw_config: TypeItem, properties: Properties) -> Result<AnnotationCodegen, darling::Error> {
    let Properties {
        id,
        pavex,
        key,
        cloning_policy,
        default_if_missing,
        include_if_unused,
    } = properties;

    let name = raw_config.name();
    // Use the span of the type name if no identifier is provided.
    let id_span = id.as_ref().map(|id| id.span()).unwrap_or(name.span());
    // If the user didn't specify an identifier, generate one based on the type name.
    let id = id.unwrap_or_else(|| format_ident!("{}", name.to_string().to_case(Case::Constant)));

    if id == name {
        return Err(darling::Error::custom(
            "The name of your type clashes with the name of the constant generated by this macro.\n\
            Specify a different `id` to resolve the issue.",
        ));
    }

    let id_str = id.to_string();

    let mut properties = quote! {
        id = #id_str,
        key = #key,
    };
    if let Some(cloning_policy) = cloning_policy {
        properties.extend(quote! {
            cloning_policy = #cloning_policy,
        });
    }
    if default_if_missing {
        properties.extend(quote! {
            default_if_missing = true,
        });
    }
    if include_if_unused {
        properties.extend(quote! {
            include_if_unused = true,
        });
    }

    let id_docs = format!(
        r#"A strongly-typed id to register [`{name}`] as a configuration type to your Pavex application.

# Example

```rust,ignore
use pavex::Blueprint;
// [...]
// ^ Import `{id}` here

let mut bp = Blueprint::new();
// Add `{name}` as a configuration to your application.
bp.config({id});
```"#
    );
    let pavex = match pavex {
        Some(c) => quote! { #c },
        None => quote! { ::pavex },
    };
    let id_def = quote_spanned! { id_span =>
        #[doc = #id_docs]
        #[allow(unused)]
        pub const #id: #pavex::blueprint::Config = #pavex::blueprint::Config {
            coordinates: #pavex::blueprint::reflection::AnnotationCoordinates {
                id: #id_str,
                created_at: #pavex::created_at!(),
                macro_name: "config",
            }
        };
    };

    Ok(AnnotationCodegen {
        id_def: Some(id_def),
        new_attributes: vec![parse_quote!(#[diagnostic::pavex::config(#properties)])],
    })
}
